package graphicsExtended.matrix;

import array.ArrayFunctions;
import java.awt.Point;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import list.FixedList;
import math.mathExtended.MathExtended;

/**
 *
 * @author Shimu
 * @date 10-Aug-2012
 */
public class Table<T> implements Iterable<T> {

    /** Maximum number of columns and rows */
    private final int maxX, maxY;

    // Maximum index into the allObjects array
    private final int totalCells;

    // Arrays storing T Objects
    private final T[] allObjects;

    public Table(int maxX, int maxY) {
        this.maxX = maxX;
        this.maxY = maxY;

        this.totalCells = maxX * maxY;
        this.allObjects = (T[]) new Object[totalCells];
        
        //System.out.printf("maxX, maxY: (%d, %d)\n", maxX, maxY);
    }

    /**
     * The total number of cells in a row.
     * 
     * @return the maxX
     */
    public int getTotalColumns() {
        return maxX;
    }

    /**
     * The total number of cells in a column
     * @return the maxY
     */
    public int getTotalRows() {
        return maxY;
    }

    /**
     * Returns the total number of cells in this Table
     * @return 
     */
    public int getTotalCells() {
        return totalCells;
    }

    /**
     * Returns the T Object object at the given x, y coordinate. Returns
     * null if no T object occupies the given x, y coordinate.
     *
     * @param x 
     * @param y 
     * @throws IndexOutOfBoundsException if the given x, y values are outside
     * the bounds of this grid
     * @return
     */
    public T get(int x, int y) {
        return allObjects[getIndex(x, y)];
    }

    /**
     * Sets the given T to the given (x, y) coordinate of the grid.
     * Returns the previous T that was at the given (x, y) coordinate.
     * 
     * @param x the x-coordinate of the object
     * @param y the y-coordinate of the object
     * @param s the new T to put in the given x and y
     * @throws IndexOutOfBoundsException if the given x, y values are outside
     * the bounds of this grid
     * @return the previous T that was at the given x and y,
     * T.SENTINEL_T if no previous T was occupying 
     * the given x and y.
     */
    public T set(int x, int y, T s) {
        return ArrayFunctions.replace(allObjects, getIndex(x, y), s);
    }

    public T remove(int x, int y) {        
        return ArrayFunctions.replace(allObjects, getIndex(x, y), null);
    }
    
    /**
     * Returns true if the given x, y coordinates are valid coordinates 
     * within the grid.
     * 
     * @param x
     * @param y
     * @return 
     */
    public boolean isValidCoordinate(int x, int y) {
        return MathExtended.bounded(x, 0, maxX - 1)
                && MathExtended.bounded(y, 0, maxY - 1);
    }

    /**
     * Returns true if the given (x, y) coordinate is empty.
     * @param x
     * @param y
     * @return 
     */
    public boolean isEmpty(int x, int y) {
        return this.get(x, y) == null;
    }

    @Override
    public Iterator<T> iterator() {
        return Arrays.asList(allObjects).iterator();
    }

    // ===================== MAPPING METHODS =====================
    private class ColumnList extends FixedList<T> {

        private final int COLUMN_INDEX;

        public ColumnList(int columnIndex, Table grid) {
            super(maxY);
            this.COLUMN_INDEX = columnIndex;
        }

        @Override
        public T get(int index) {
            return Table.this.get(COLUMN_INDEX, index);
        }

        @Override
        public T set(int index, T element) {
            return Table.this.set(COLUMN_INDEX, index, element);
        }

        @Override
        public FixedList<T> subList(int fromIndex, int toIndex) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    }

    public FixedList<T> getColummAsList(int columnIndex) {
        return new ColumnList(columnIndex, this);
    }

    public List<T> getRowAsList(int rowIndex) {
        return Arrays.asList(allObjects).subList(rowIndex, rowIndex + maxX);
    }

    public List<T> asList() {
        return Arrays.asList(allObjects);
    }

    public void setAll(Factory<T> factory) {
        for (int x = 0; x < this.maxX; x++) {
            for (int y = 0; y < this.maxY; y++) {
                this.set(x, y, factory.create());
            }
        }
    }
    
    // ===================== PROTECTED BOOLEAN METHODS =====================

    /**
     * Return true if the given Point p, represents the same coordinate
     * as the given (x, y) coordinate.
     * 
     * @param p
     * @param x
     * @param y
     * @return 
     */
    protected boolean equal(Point p, int x, int y) {
        return (p.x == x) && (p.y == y);
    }

    /**
     * Returns true if x1 = x2 and y1 = y2.
     * 
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @return 
     */
    protected boolean equal(int x1, int y1, int x2, int y2) {
        return (x1 == x2) && (y1 == y2);
    }

    /**
     * Check the given x, y coordinate, if it is invalid, throw an
     * IndexOutOfBoundsException. Does nothing if the coordinates are valid.
     * 
     * @param x
     * @param y 
     * @throws IndexOutOfBoundsException if the x, y coordinates are invalid
     */
    protected void checkCoordinate(int x, int y) {
        if (!isValidCoordinate(x, y)) {
            throw new IndexOutOfBoundsException(String.format("(%d, %d)", x, y));
        }
    }

    protected boolean isEmpty(int index) {
        return allObjects[index] == null;
    }
    
    // ===================== PROTECTED GETTERS/SETTERS =====================
    /**
     * Given the x and y value of an object on the grid, returns the index 
     * into the array. 
     * 
     * @param x the x-coordinate of the object
     * @param y the y-coordinate of the object
     * @throws IndexOutOfBoundsException if the given x, y values are outside
     * the bounds of this grid
     * @return the index into the array
     */
    protected int getIndex(int x, int y) {
        checkCoordinate(x, y);
        return x + y * getTotalColumns();
    }

    /**
     * Given the index into the array, returns the x-coordinate.
     * 
     * @param index
     * @return 
     */
    protected int getX(int index) {
        return index % maxX;
    }

    /**
     * Given the index into the array, returns the y-coordinate.
     * 
     * @param index
     * @return 
     */
    protected int getY(int index) {
        return index / maxX;
    }

    protected T getObject(int index) {
        return allObjects[index];
    }
    
    /**
     * Sets the given T to the given index of the grid.
     * Returns the previous T that was at the given index.
     * 
     * @param index index into the Table (indexing starts a 0 at coordinate 
     * (0, 0) increases horizontally across a row until (0, maxX - 1) in which 
     * case the cursor moves one row down to (1, 0) and repeats until 
     * (maxX - 1, maxY - 1). 
     * @param s the new T to put in the given index
     * @throws IndexOutOfBoundsException if the given index is outside
     * the bounds of this grid
     * @return the previous T that was at the given index,
     * T.SENTINEL_T if no previous T was occupying 
     * the given index.
     */
    protected T setObject(int index, T s) {
        return ArrayFunctions.replace(allObjects, index, s);
    }
}
